/// \file  VideoMode_Main.cpp
/// \brief main() for video mode console application using CATVideoMode. 
///
/// See docs below in constants for help.
///
///
//
// The MIT License
//
// Copyright (c) 2008 by Michael Ellison
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//
// $Author: devellison $
// $Date: 2008-03-31 07:50:33 +0000 (Mon, 31 Mar 2008) $
// $Revision:   $
// $NoKeywords: $

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include "CATVideoMode.h"

const wchar_t* kHelpPreface[] = {
L"CATVideoMode 0.2 Copyright (c) 2008 by Michael Ellison.",
L"http://catvideomode.googlecode.com",
L"",
0
};

const wchar_t* kHelpShort[] = {
L"Type 'CATVideoMode /?' for usage.",
0
};

const wchar_t* kHelpLong[] = {
L"This command-line utility may be used to retrieve available",
L"display modes in a system or set them.",
L"",
L"Flags:",
L"~~~~~~",
L"    /p  Pause after each page for a key (MUST be first switch if present)",
L"    /a  Show all modes (otherwise only reports modes monitor reports)",
L"",
L"Commands:",
L"~~~~~~~~~",
L"    /h  Show usage help.",
L"    /?  Show usage help.",
L"    /c  Get the current modes for all displays",
L"    /v  Get the current modes for all displays (verbose)",
L"",
L"    /g  Get the available video modes.",
L"    /s  Set the video mode.",
L"",
L"Commands may be chained to perform multiple commands on a",
L"single line.",
L"",
L"",
L"Command Parameters:",
L"~~~~~~~~~~~~~~~~~~~",
L"No extra parameters are taken for \"/h\", \"/?\", \"/c\", or \"/v\".",
L"",
L"For \"/s\" and \"/g\", all parameters are optional. Parameters that",
L"are specified must be in order.  If you don't wish to specify a",
L"specific parameter in the sequence, provide the default value",
L"(usually 0, but stated below with each parameter). ",
L"",
L"NOTE: For rotated modes, the width and height reflect the",
L"      rotation. E.g. a 1280x1024 display at 90 degrees will",
L"      have a width of 1024, and a height of 1280",
L"",
L"Get Modes Syntax:",
L"~~~~~~~~~~~~~~~~~~",
L"    CATVideoMode [/a] /g display width height refresh bpp rotation",
L"",
L"    \"display\"  is the display index (zero based), indicating the ",
L"               display to get/set the mode on. \"-1\" is the default",
L"               which indicates all displays.",
L"",
L"    \"width\"    is the pixel width, or 0 to indicate any [default].",
L"",
L"    \"height\"   is the pixel height, or 0 to indicate any [default].",
L"",
L"    \"refresh\"  is the refresh rate, or 0 to indicate any [default].",
L"",
L"    \"bpp\"      is the # of bits per pixel, or 0 to indicate any [default].",
L"",
L"    \"rotation\" is the rotation of the mode.",
L"                 -1 indicates any rotation.",
L"                 0 specifies no rotation [default].",
L"                 90, 180, and 270 indicate rotation in degrees.",
L"                 Rotations are counter-clockwise with 0 at top.",
L"                 1, 2, and 3 may also be used for 90, 180, and 270.",
L"",
L"NOTE: If you put a \"/a\" on the command line prior to the \"/g\"",
L"command, all video modes on the card will be displayed. Otherwise,",
L"only modes supported by the detected monitor will be displayed.",
L"",
L"Fields set to 'any' will match any mode parameters for the type.",
L"",
L"Set Mode Syntax:",
L"~~~~~~~~~~~~~~~~~~",
L"    CATVideoMode /s display width height refresh bpp rotation",
L"",
L"The parameters for the set command, \"/s\", are essentially the same",
L"as the get command - \"/g\". Where the get command matches the 'any'",
L"values as wildcards, the set command simply doesn't set those fields",
L"when attempting to change the mode.",
L"",
L"Examples:",
L"~~~~~~~~~",
L"To get all supported full-color modes on all monitors and pause each page:",
L"    CATVideoMode /p /g -1 0 0 0 32 -1",
L"",
L"To set the mode on the first display to 1600x1200, 32-bit color, 70Hz:",
L"    CATVideoMode /s 0 1600 1200 70 32",
L"",
L"To set the mode on the second display to 768x1024 32-bit 60Hz (rotated 90):",
L"    CATVideoMode /s 1 768 1024 60 32 90",
L"",
L"To set all displays to 1280x1024:",
L"    CATVideoMode /s -1 1280 1024",
L"",
L"To get information about the current mode on all cards:",
L"    CATVideoMode /c",
L"",
L"To set the first display to 1600x1200, and the second to 1280x1024:",
L"    CATVideoMode /s 0 1600 1200 /s 1 1280 1024",
L"",
L"To see this help again with page breaks, try:",
L"    CATVideoMode /p /?\n",
0
};


int gPageBreaks    = 0;
int gConsoleHeight = 25;
int gCurRow        = 0;

// main command functions
CVM_RESULT GetVideoModes(int& start, int argc, char** argv, CATVideoMode* cvm);
CVM_RESULT SetVideoMode (int& start, int argc, char** argv, CATVideoMode* cvm);

CVM_RESULT GetCurrent        (CATVideoMode* cvm);
CVM_RESULT GetCurrentVerbose (CATVideoMode* cvm);

void       OnHelp       (bool showFullHelp);

// parameter helpers
void GetModeParams(int&         start, 
                   int          argc, 
                   char**       argv,
                   int&         display,
                   int&         width,
                   int&         height,
                   int&         refresh,
                   int&         bpp,
                   CVM_ORIENT&  rot);


bool GetNextParam (int& start, int argc, char** argv, int& param);

// print helpers
void wprintLine(const wchar_t* lineString, ...);
void CVMPrintError(CVM_RESULT result);
const wchar_t* OrientToDeg(CVM_ORIENT orient);

int main(int argc, char** argv)
{
    bool realize      = false;
    bool showFullHelp = false;

    CATVideoMode cvm;
    cvm.Initialize(true);

    if (argc == 1)
    {        
        OnHelp(false);
    }

    CVM_RESULT result = CVM_SUCCESS;

    // Loop through commands (if any), executing each in order.
    //
    // Note that flag commends (/p, /a) MUST be set before
    // the command that they should effect.
    for (int i = 1; i < argc; i++)
    {
        CVM_RESULT tmpResult = CVM_ERR_INVALID_PARAM;

        if (argv[i][0] == '/')
        {
            switch (argv[i][1])
            {
                // page breaks - not fully supporting console yet,
                // but at least break at pages if they're using
                // standard 80 column display.
                case 'p': 
                    gPageBreaks    = true;
                    gConsoleHeight = 25;
                    {
                        
                        CONSOLE_SCREEN_BUFFER_INFO bufferInfo;
                        ::GetConsoleScreenBufferInfo(
                            ::GetStdHandle(STD_OUTPUT_HANDLE),
                            &bufferInfo);

                        gConsoleHeight = bufferInfo.srWindow.Bottom
                                         - bufferInfo.srWindow.Top;
                    }
                    tmpResult = CVM_SUCCESS;
                    break;
                // retrieve all modes, not just monitor-supported
                case 'a':
                    tmpResult = cvm.Initialize(false);
                    break;
                
                // get current modes (verbose)
                case 'v':
                    tmpResult = GetCurrentVerbose(&cvm);
                    break;

                // get current modes
                case 'c':
                    tmpResult = GetCurrent(&cvm);
                    break;

                // get matching modes
                case 'g':
                    tmpResult = GetVideoModes(i,argc,argv,&cvm);                    
                    break;

                // set mode
                case 's':
                    tmpResult = SetVideoMode(i,argc,argv,&cvm);
                    realize = true;
                    break;

                // print help
                case 'h':
                case '?':
                    OnHelp(true);
                    tmpResult = CVM_SUCCESS;
                    break;
            }
        }
        
        if (CVM_FAILED(tmpResult))
            result = tmpResult;
    }

    if (realize)
    {
        cvm.RealizeDisplayModes();
    }

    if (result != CVM_SUCCESS)
        CVMPrintError(result);

    return result;
}

CVM_RESULT GetVideoModes(int& start, int argc, char** argv, CATVideoMode* cvm)
{
    int display = -1;
    int width   = 0;
    int height  = 0;
    int refresh = 0;
    int bpp     = 0;
    CVM_ORIENT rot = CVM_ROT_NONE;
    
    GetModeParams(start,argc,argv,display,width,height,refresh,bpp,rot);

    std::vector<CVM_INFO_ADAPTER> modeList;
    CVM_RESULT result = cvm->GetModes(  display,
                                        modeList,
                                        width,
                                        height,
                                        refresh,
                                        bpp,
                                        rot);

    if (CVM_FAILED(result))
    {
        return result;
    }

    int lastDisplay = -1;

    for (CVMUInt32 i = 0; i < modeList.size(); i++)
    {
        if (lastDisplay != modeList[i].DisplayNumber)
        {
            lastDisplay = modeList[i].DisplayNumber;

            wprintLine(L"\nDisplay %3d: %s [%s] (%s)",
                  modeList[i].DisplayNumber,
                  modeList[i].DisplayName,
                  modeList[i].AdapterName,                  
                  modeList[i].DriverVersion);            
        }
        wprintLine(L"%d: [%4d,%4d] @ %3dHz %2d %s",
            modeList[i].DisplayNumber,
            modeList[i].WidthPixels,
            modeList[i].HeightPixels,
            modeList[i].RefreshRateHz,
            modeList[i].BitsPerPixel,
            OrientToDeg(modeList[i].Orientation));
    }

    modeList.clear();

    return CVM_SUCCESS;
}

CVM_RESULT SetVideoMode (int& start, int argc, char** argv, CATVideoMode* cvm)
{
    int display     = -1;
    int width       = 0;
    int height      = 0;
    int refresh     = 0;
    int bpp         = 0;
    CVM_ORIENT rot  = CVM_ROT_NONE;
    
    GetModeParams(start,argc,argv,display,width,height,refresh,bpp,rot);

    CVM_RESULT result = cvm->SetDisplayMode(display,
                                            width,
                                            height,
                                            bpp,
                                            refresh,
                                            rot,
                                            true);

    if (CVM_FAILED(result))
    {
        wprintLine(L"An error occurred (0x%08x) settings the mode.",
               result);
        
        return result;
    }

    return CVM_SUCCESS;
}

CVM_RESULT GetCurrentVerbose(CATVideoMode* cvm)
{
    CVM_RESULT result;

    CVM_INFO_ADAPTER_CURRENT info;

    CVMUInt32 numDisplays = cvm->GetNumberOfDisplays();
    for (CVMUInt32 i = 0; i < numDisplays; i++)
    {
        result = cvm->GetCurrentDisplayMode(i,&info);

        wprintLine(L"");
        wprintLine(L"Display %3d: %s [%s] (%s)",
            info.DisplayNumber,
            info.DisplayName,
            info.AdapterName,                  
            info.DriverVersion);

        wprintLine(L"\tBrand:     %s", cvm->CardBrandToString(info.CardBrand));

        wprintLine(L"\tMode:      [%4dx%4d] @ %3dHz %2dbpp %s deg",
            info.WidthPixels,
            info.HeightPixels,
            info.RefreshRateHz,
            info.BitsPerPixel,
            OrientToDeg(info.Orientation));
      
        wprintLine(L"\tPlanes:    %d", info.BitPlanes);
        wprintLine(L"\tOrigin:    %d,%d", info.Left, info.Top);
        
        wprintLine(L"\tWork area: (%d,%d-%d,%d)", 
            info.WorkLeft,
            info.WorkTop,
            info.WorkWidth + info.WorkLeft,
            info.WorkHeight + info.WorkTop);
        
        wprintLine(L"\tPrimary:   %s",
            info.Primary?L"Yes":L"No");
    }

    return CVM_SUCCESS;
}

// stripped down text output for current modes, can be passed
// back in.
CVM_RESULT GetCurrent(CATVideoMode* cvm)
{
    CVM_RESULT result;
    CVM_INFO_ADAPTER_CURRENT info;
    CVMUInt32 numDisplays = cvm->GetNumberOfDisplays();
    for (CVMUInt32 i = 0; i < numDisplays; i++)
    {
        result = cvm->GetCurrentDisplayMode(i,&info);

        wprintf(L" /s %d %d %d %d %d ",
            info.DisplayNumber,
            info.WidthPixels,
            info.HeightPixels,
            info.RefreshRateHz,
            info.BitsPerPixel);
        
        if (info.Orientation != CVM_ROT_ANY)
            wprintf(L"%s",OrientToDeg(info.Orientation));
    }

    return CVM_SUCCESS;
}

void GetModeParams(int&         start, 
                   int          argc, 
                   char**       argv,
                   int&         display,
                   int&         width,
                   int&         height,
                   int&         refresh,
                   int&         bpp,
                   CVM_ORIENT&  rot)
{
    display    = -1;
    width      = 0;
    height     = 0;
    refresh    = 0;
    bpp        = 0;
    rot        = CVM_ROT_NONE;
    
    int orientDeg  = 0;

    GetNextParam(start,argc,argv,display);
    GetNextParam(start,argc,argv,width);
    GetNextParam(start,argc,argv,height);
    GetNextParam(start,argc,argv,refresh);
    GetNextParam(start,argc,argv,bpp);
    GetNextParam(start,argc,argv,orientDeg);

    switch (orientDeg)
    {        
        case 1:
        case 90:
            rot = CVM_ROT_90;
            break;
        case 2:
        case 180:
            rot = CVM_ROT_180;
            break;
        case 3:
        case 270:
            rot = CVM_ROT_270;
            break;
        case 0:
            rot = CVM_ROT_NONE;
            break;
        
        case -1:
        default:
            rot = CVM_ROT_ANY;
            break;
    }
}

bool GetNextParam(int&          start, 
                  int           argc, 
                  char**        argv, 
                  int&          param)
{
    // Return false if at end of command line
    if (start + 1 >= argc) 
        return false;
 
    // return false if the argument is a new command
    if (argv[start+1][0] == '/')
        return false;

    // decode param from ascii to decimal, increment argument, return.
    param = atoi(argv[start+1]); 
    start++;

    return true;
}

void OnHelp(bool fullHelp)
{
    const wchar_t** curMsg = kHelpPreface;
    
    CVMUInt32 i = 0;
    
    while (curMsg[i] != 0)
    {
        wprintLine(curMsg[i]);
        ++i;
    }

    if (!fullHelp)
    {
        curMsg = kHelpShort;
    }
    else
    {
        curMsg = kHelpLong;
    }

    i = 0;
    while (curMsg[i] != 0)
    {
        wprintLine(curMsg[i]);
        ++i;
    }
}

const wchar_t* OrientToDeg(CVM_ORIENT orient)
{
    switch (orient)
    {
        case CVM_ROT_ANY:  return L"Any";
        case CVM_ROT_NONE: return L"0";
        case CVM_ROT_90:   return L"90";
        case CVM_ROT_180:  return L"180";
        case CVM_ROT_270:  return L"270";
    }
    return L"Invalid Rotation";
}

void wprintLine(const wchar_t* lineString, ...)
{
    va_list fmtArgs;
    va_start(fmtArgs, lineString);
   
    vwprintf(lineString,fmtArgs);
    wprintf(L"\n");
    gCurRow++;
    if (gPageBreaks)
    {
        if (gCurRow == gConsoleHeight)
        {
            wprintf(L"Press any key to continue....");
            _getch();
            // clear press any key line and reset count
            wprintf(L"\r%*c\r",79,0x20);
            gCurRow = 0;
        }
    }

    va_end(fmtArgs);
}

void CVMPrintError(CVM_RESULT result)
{
    switch (result)
    {
        case CVM_SUCCESS:
            wprintLine(L"Success");
            break;
        case CVM_STATUS:
            wprintLine(L"Status code");
        case CVM_STATUS_ALT_MODE:
            wprintLine(L"Alternative mode was set");
            break;
        case CVM_STATUS_LINKED:
            wprintLine(L"Display is linked");
            break;
        case CVM_ERROR:
            wprintLine(L"Unknown error.");
            break;
        case CVM_ERR_INVALID_PARAM:
            wprintLine(L"Invalid parameter");
            break;
        case CVM_ERR_INVALID_DISPLAY:
            wprintLine(L"Invalid display number");
            break;
        case CVM_ERR_CANNOT_BREAK_LINK:
            wprintLine(L"Channels could not be unlinked.");
            break;
        case CVM_ERR_NO_MATCHING_DISPLAY:
            wprintLine(L"No matching display found.");
            break;
        case CVM_ERR_INVALID_WND:
            wprintLine(L"Invalid window.");
            break;
        case CVM_ERR_SET_MODE:
            wprintLine(L"General error setting mode.");
            break;
        case CVM_ERR_NVIDIA_WONT_ROTATE_BELOW_1024_768:
            wprintLine(L"NVidia cards don't like rotating below 1024x768.");
            break;
        case CVM_ERR_SET_MODE_DUAL_VIEW:
            wprintLine(L"Set failed because display in DualView");
            break;
        case CVM_ERR_SET_MODE_BAD_FLAGS:
            wprintLine(L"Set failed - bad flags.");
            break;
        case CVM_ERR_SET_MODE_INVALID_MODE:
            wprintLine(L"Set failed - invalid mode requested");
            break;
        case CVM_ERR_SET_MODE_BAD_PARAM:
            wprintLine(L"Set failed - bad parameter");
            break;
        case CVM_ERR_SET_MODE_FAILED:
            wprintLine(L"Set failed.");
            break;
        case CVM_ERR_SET_MODE_REGISTRY:
            wprintLine(L"Set failed - unable to write to reg");
            break;
        case CVM_ERR_SET_MODE_REBOOT:
            wprintLine(L"Reboot required for set.");
            break;

        default:
            if (result & 0x80000000)
                wprintLine(L"Unknown error.");
            else
                wprintLine(L"Non-error status (0x%08x).",result);
            break;
    }
}
